package com.gitee.dufafei.plugin.core;

import com.gitee.dufafei.plugin.consts.Const;
import com.gitee.dufafei.plugin.listener.PluginListener;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URLClassLoader;
import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class PluginRegistry {

    private static Logger logger = LoggerFactory.getLogger(PluginRegistry.class);
    private static final PluginRegistry pluginRegistry = new PluginRegistry();
    private final Map<Class<? extends PluginTypeInterface>, List<PluginListener>> listeners;
    private final Map<Class<? extends PluginTypeInterface>, List<PluginInterface>> plugins;
    private final Map<Class<? extends PluginTypeInterface>, List<String>> categories;
    private final Map<Class<? extends PluginTypeInterface>, Map<PluginInterface, URLClassLoader>> classLoaderMap;
    private final ReentrantReadWriteLock lock;

    private PluginRegistry() {
        listeners = new HashMap<>();
        plugins = new HashMap<>();
        categories = new HashMap<>();
        classLoaderMap = new HashMap<>();
        lock = new ReentrantReadWriteLock();
    }

    public static PluginRegistry getInstance() { return pluginRegistry; }

    public void register(PluginTypeInterface pluginType) throws Exception {
        registerPluginType(pluginType);
        List<PluginListener> listeners = pluginType.getListeners();
        for (PluginListener listener: listeners) {
            registerListener(pluginType, listener);
        }
        List<PluginInterface> plugins = pluginType.getPlugins();
        for (PluginInterface plugin: plugins) {
            registerPlugin(pluginType, plugin);
        }
    }

    public void remove(PluginTypeInterface pluginType) { removePluginType(pluginType); }

    public void registerPluginType(PluginTypeInterface pluginType) {
        lock.writeLock().lock();
        try {
            listeners.put(pluginType.getClass(), new ArrayList<>());
            plugins.put(pluginType.getClass(), new ArrayList<>());
            categories.put(pluginType.getClass(), new ArrayList<>());
            classLoaderMap.put(pluginType.getClass(), new HashMap<>());
        } finally {
            lock.writeLock().unlock();
        }
    }

    public void removePluginType(PluginTypeInterface pluginType) {
        lock.writeLock().lock();
        try {
            listeners.remove(pluginType.getClass());
            plugins.remove(pluginType.getClass());
            categories.remove(pluginType.getClass());
            classLoaderMap.remove(pluginType.getClass());
        } finally {
            lock.writeLock().unlock();
        }
    }

    public void registerListener(PluginTypeInterface pluginType, PluginListener listener) {
        List<PluginListener> listeners = this.listeners.get(pluginType.getClass());
        if (listeners == null) {
            throw new PluginException("The plug-in type is not registered.");
        } else {
            listeners.add(listener);
        }
    }

    public void registerPlugin(PluginTypeInterface pluginType, PluginInterface plugin) {
        boolean changed = false;
        lock.writeLock().lock();
        try {
            if (plugin.getId() == null) {
                throw new PluginException("Not a valid id specified in plugin: " + plugin);
            }
            List<PluginInterface> plugins = this.plugins.get(pluginType.getClass());
            if (plugins == null) {
                throw new PluginException("The plug-in type is not registered.");
            } else {
                int index = plugins.indexOf(plugin);
                if (index < 0) {
                    logger.info("The plug-in has been added: " + plugin.getId());
                    plugins.add(plugin);
                } else {
                    logger.info("The plug-in has been modified: " + plugin.getId());
                    plugins.set(index, plugin);
                    changed = true;
                }
                plugins.sort((o1, o2) -> {
                    if (o1.getName() != null && o2.getName() != null) {
                        return o1.getName().compareToIgnoreCase(o2.getName());
                    } else {
                        return o1.getName() == null ? -1 : 1;
                    }
                });
                if (StringUtils.isNotEmpty(plugin.getCategory())) {
                    List<String> categories = this.categories.get(pluginType.getClass());
                    if (!categories.contains(plugin.getCategory())) {
                        categories.add(plugin.getCategory());
                        PluginCategoriesOrder naturalOrderAnnotation = pluginType.getClass().getAnnotation(PluginCategoriesOrder.class);
                        if (naturalOrderAnnotation != null) {
                            final String[] fNaturalOrder = naturalOrderAnnotation.naturalCategoriesOrder();
                            categories.sort((o1, o2) -> {
                                int idx1 = Const.indexOfString(o1, fNaturalOrder);
                                int idx2 = Const.indexOfString(o2, fNaturalOrder);
                                return (idx1 == -1 || idx2 == -1) ? (idx2 - idx1) : (idx1 - idx2);
                            });
                        }
                    }
                }
            }
        } finally {
            lock.writeLock().unlock();
            List<PluginListener> listeners = this.listeners.get(pluginType.getClass());
            for (PluginListener listener : listeners) {
                if (changed) {
                    listener.onChange(plugin);
                } else {
                    listener.onAdd(plugin);
                }
            }
            synchronized (this) {
                notifyAll();
            }
        }
    }

    public void removePlugin(PluginTypeInterface pluginType, PluginInterface plugin) {
        lock.writeLock().lock();
        try {
            List<PluginInterface> plugins = this.plugins.get(pluginType.getClass());
            if (plugins == null) {
                throw new PluginException("The plug-in type is not registered.");
            } else {
                logger.info("The plug-in has been removed: " + plugin.getId());
                plugins.remove(plugin);
            }
        } finally {
            lock.writeLock().unlock();
            List<PluginListener> listeners = this.listeners.get(pluginType.getClass());
            if (listeners != null) {
                for (PluginListener listener : listeners) {
                    listener.onRemove(plugin);
                }
            }
            synchronized (this) {
                notifyAll();
            }
        }
    }

    public <T extends PluginTypeInterface> List<PluginInterface> getPlugins(Class<T> pluginType) {
        lock.readLock().lock();
        try {
            List<PluginInterface> plugins = this.plugins.get(pluginType);
            if (plugins == null) {
                throw new PluginException("The plug-in type is not registered.");
            } else {
                return Collections.unmodifiableList(plugins);
            }
        } finally {
            lock.readLock().unlock();
        }
    }

    public <T extends PluginTypeInterface> List<PluginInterface> getPluginsByCategory(Class<T> pluginType, String category) {
        List<PluginInterface> plugins = new ArrayList<>();
        for ( PluginInterface verify : getPlugins(pluginType)) {
            if (verify.getCategory() != null && verify.getCategory().equals(category)) {
                plugins.add(verify);
            }
        }
        return Collections.unmodifiableList(plugins);
    }

    public PluginInterface getPluginById(Class<? extends PluginTypeInterface> pluginType, String id) {
        if (StringUtils.isEmpty(id)) {
            return null;
        }
        PluginInterface sp = null;
        for (PluginInterface plugin : getPlugins(pluginType)) {
            if (plugin.getId().equals(id)) {
                sp = plugin;
                break;
            }
        }
        return sp;
    }

    public List<String> getCategories(Class<? extends PluginTypeInterface> pluginType) {
        lock.readLock().lock();
        try {
            List<String> categories = this.categories.get(pluginType);
            if (categories == null) {
                throw new PluginException("The plug-in type is not registered.");
            } else {
                return Collections.unmodifiableList(categories);
            }
        } finally {
            lock.readLock().unlock();
        }
    }

    @SuppressWarnings("unchecked")
    public <T> T loadClass(PluginInterface plugin, String className) {
        if (plugin == null) {
            throw new PluginException("The type of PluginInterface parameter cannot be null.");
        } else {
            Class<? extends T> cl;
            try {
                if (plugin.isNative()) {
                    cl = (Class<? extends T>) Class.forName(className);
                } else {
                    URLClassLoader ucl;
                    lock.writeLock().lock();
                    try {
                        // See if we can find a class loader to re-use.
                        Map<PluginInterface, URLClassLoader> classLoaders = classLoaderMap.get(plugin.getPluginType());
                        ucl = classLoaders.get(plugin);
                        if (ucl == null) {
                            ClassLoader classLoader = getClass().getClassLoader();
                            ucl = new PluginURLClassLoader(plugin.getLibraries(), classLoader);
                            // save for later use...
                            classLoaders.put(plugin, ucl);
                        }
                    } finally {
                        lock.writeLock().unlock();
                    }
                    cl = (Class<? extends T>) ucl.loadClass(className);
                }
                return cl.newInstance();
            } catch (Exception e) {
                throw new PluginException("Failed to get the plugin instance.", e);
            }
        }
    }

    public <T> T loadClass(PluginInterface plugin) { return loadClass(plugin, plugin.getClassName()); }
}
